package parser;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.NoSuchElementException;

import javax.naming.OperationNotSupportedException;

import utils.GNDUtils;
import utils.MyStringUtils;
import utils.Triplett;
import exceptions.IllFormattedLineException;

/**
 * 
 * @author baumann
 *	
 *	Nimmt an, dass ein {@link BufferedReader} Datensätze und Kommandos in
 *	beliebiger Reihenfolge enthält. Der Aufrufer weiß aber, was als nächstes
 *	kommt (Datensatz oder Kommando). Das nächste Kommando kann mit 
 *	getNextCommand(), der nächste Datensatz mit getNextRecord() aufgerufen 
 *	werden.
 *	
 *	Kommando und Datensatz werden über ihr jeweiliges Präfix identifiziert. Die
 *	Präfices können über set...Prefix() geändert werden.
 *
 *	Der Datensatz besteht aus Feldern, die über mehrere Zeilen
 *	gehen können. Jedes Feld besteht aus dem initialen TagDB und dem Inhalt.
 *
 */
public final class RecordParser {

	private static final int BUFFER_SIZE = 1024;

	// --- zum Parsen benötigt -------
	private String rawData;

	private boolean isNewLine = false;

	private Tag oldTag = null;

	private Tag newTag = null;

	private String oldContent = "";

	private String newContent = "";

	private boolean isEndOfRecord = false;

	// --- Können von außen verändert werden ----	

	private String recordPrefix = "SET: ";

	private BufferedReader reader =
		new BufferedReader(new InputStreamReader(System.in));

	// ------------------------------------------

	/**
	 * Erzeugt einen Datensatz aus einer Stringrepräsentation. Die idn wird als
	 * bekannt vorausgesetzt.
	 * 
	 * @param data
	 * @param id
	 * @return
	 * 
	 */
	public Record getRocordFromString(final String data, final String id) {
		if (data == null)
			throw new IllegalArgumentException("Daten sind null");
		Record dsv = null;
		try {
			reader = new BufferedReader(new StringReader(data));
			dsv = getRecord(id);
		} catch (IOException e) {
			// gar nichts, da String
		}
		dsv.id = id;
		return dsv;
	}

	/**
	 * Gibt einen gültigen Datensatz oder null.
	 * 
	 * @param br
	 * @return	Datensatz oder null am Ende von br.
	 * @throws IOException
	 */
	public Record getNextRecord() throws IOException

	{

		String line = getNextNonEmptyLine();
		if (line == null)
			return null;
		reader.mark(BUFFER_SIZE); // um br zurücksetzen zu können.

		// jetzt beginnt der Datensatz, daher ein Versuch, die idn zu ermitteln:
		String idn = MyStringUtils.extractIDN(line);

		return getRecord(idn);
	}

	/**
	 * Geht zur ersten nicht leeren Zeile und gibt diese oder null zurück.
	 * 
	 * @param br		nicht null            
	 * @return 			erste nicht leere Zeile oder null
	 * @throws IOException
	 */
	private String getNextNonEmptyLine() throws IOException {

		// Leere Zeilen zu Beginn überspringen:
		String line;
		do {
			line = reader.readLine();
			if (line == null)
				return null;
		} while (line.trim().length() == 0);
		return line;
	}

	/**
	 * Geht nicht davon aus, dass eine IDN in br enthalten ist.
	 * 
	 * Die idn ist eventuell schon anderweitig ermittelt worden, z.B. in der
	 * aufrufenden Funktion getNextRecord().
	 *
	 * @param idn
	 * @return
	 * @throws IOException
	 * @throws NoSuchElementException
	 */
	public Record getRecord(final String idn) throws IOException {

		// Initialisieren der Variablen
		isEndOfRecord = false;
		isNewLine = false;
		oldTag = null;
		newTag = null;
		oldContent = null;
		newContent = null;
		rawData = "";

		Record record = new Record();

		record.id = idn;
		/*
		 *  br Zeile für Zeile lesen und analysieren. Wenn neuerDatensatz == true,
		 *  oder Stream zu Ende, den Rest auswerten,  
		 *  die Factory verlassen und den Eingabestrom zurücksetzen.
		 */
		String line;
		do {
			reader.mark(BUFFER_SIZE); // um br zurücksetzen zu können.
			line = getNextNonEmptyLine();
			// Ende des Streams:
			if (line == null)
				break;
			buildLine(line);
			// Das sollte der Normalfall sein: jede Zeile ein neues Feld:
			if (isNewLine) {
				addLine(record, oldTag, oldContent);
			}
			// Ende des Datensatzes:
			if (isEndOfRecord) {
				reader.reset();
				break;
			}
		} while (true);
		/*so oder so ist das Ende des Satzes erreicht,
		 daher den letzten TagDB noch verarbeiten: */
		addLine(record, newTag, newContent);
		record.rawData = rawData;
		return record;

	}

	/**
	 * Zerlegt line in den Tag am Anfang und dem Rest.
	 * Der Tag wird in tag, der Rest in content gespeichert. Beginnt line 
	 * nicht mit einem Tag, so wird sie an content angehängt.
	 * 
	 * Zwei globale Variable werden verändert:
	 * 
	 * 1. Satzende erreicht: 				isEndOfRecord = true
	 * 2. Zeile beginnt mit einem Tag: 		isNewField = true
	 * 
	 * @param lineStr	Eine nicht leere Zeile, eventuell mit einem Tag 
	 * 					beginnend.
	 */
	private void buildLine(final String lineStr) {
		if (lineStr == null)
			throw new IllegalArgumentException("übergebene Zeile ist null");

		isNewLine = false;

		// Neuer Datensatz erreicht?
		if (lineStr.startsWith(recordPrefix)) {
			isEndOfRecord = true;
			return;
		}
		rawData += lineStr + "\n";

		Triplett<Tag, String, String> triplett = TagDB.parseTag(lineStr);
		if (triplett != null) { // Neuer Zustand

			// umkopieren:
			oldTag = newTag;
			newTag = triplett.first;
			isNewLine = true;
			oldContent = newContent;
			newContent = triplett.third;
			return;

		} else
			// hängt wohl was in die nächste Zeile über, daher anfügen.
			newContent += lineStr;
	}

	private void addLine(
			final Record record,
			final Tag tag,
			final String content) {
		// Da sind wir noch am Angfang:
		if (oldTag == null)
			return;
		else {
			try {
				Line line = new Line(tag, content);
				record.add(line);
			} catch (IllFormattedLineException e) {
				GNDUtils.logError("Zeile schlecht: idn=" + record.id + " / " + tag.pica3 + " "
					+ content);
			} catch (OperationNotSupportedException e) {
				GNDUtils.logError("add verboten: idn=" + record.id + " / " + tag.pica3 + " "
					+ content);
			}
		}

	}

	public void setRecordPrefix(final String satzAnfang) {
		if (satzAnfang != null) {
			recordPrefix = satzAnfang;
			MyStringUtils.setTeilstringVorIDN(satzAnfang);
		}
	}

	public void setReader(BufferedReader reader) {
		if (reader == null)
			throw new IllegalArgumentException();
		this.reader = reader;
	}

	public void setReader(String file) throws FileNotFoundException {
		if (reader == null)
			throw new IllegalArgumentException();
		this.reader = new BufferedReader(new FileReader(file));
	}

	public RecordParser(BufferedReader reader) {
		if (reader == null)
			throw new IllegalArgumentException();
		this.reader = reader;
	}

	public RecordParser(FileReader fileReader) {
		if (fileReader == null)
			throw new IllegalArgumentException();
		this.reader = new BufferedReader(fileReader);
	}

	public RecordParser(File file) throws FileNotFoundException {
		if (file == null)
			throw new IllegalArgumentException();
		this.reader = new BufferedReader(new FileReader(file));
	}

	public RecordParser(String file) throws FileNotFoundException {
		if (file == null)
			throw new IllegalArgumentException();
		this.reader = new BufferedReader(new FileReader(file));
	}
	
	public RecordParser() {
		super();
	}

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
		RecordParser parser = new RecordParser("GNDBeispiel_P.txt");
		GNDUtils.setLogEnabled(true);
		for (Record record; null != (record = parser.getNextRecord()) ;){
			System.out.println(record);
			System.out.println();
		}
		
	}

	

}
